/**
 * Licensed under the Apache License Version 2.0.
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */
package net.interknit.knitter.cg.util;

import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.util.ArrayList;
import java.util.List;

import org.jdom.Element;

/**
 */
public class ReflectionHelper extends net.interknit.knitter.fw.util.ReflectionHelper
{
	/**
	 * Creates a unique identifier for the given Java Class. The id is created by replacing from the Class
	 * name the special characters (characters that are neither numbers nor alphabets) and white spaces with
	 * '_' character. The method * also replaces occurrences of "[]" to "Array".
	 *
	 * @param clazz
	 *            Class object representing the Java Class
	 * @return Unique identifier for the given Java Class
	 */
	public static String createUniqueId(Class<?> clazz)
	{
		return createUniqueId(getClassName(clazz));
	}

	/**
	 * Creates a unique identifier for the given Type name. The id is created by replacing special characters
	 * (characters that are neither numbers nor alphabets) and white spaces with '_' character. The method
	 * also replaces occurrences of "[]" to "Array".
	 *
	 * @param typeName
	 *            Name of a Java Type
	 * @return Unique identifier for the given Java Type
	 */
	public static String createUniqueId(String typeName)
	{
		String id = typeName;
		id = id.replaceAll(" ", "");
		id = id.replace('<', '_').replace('>', '_').replace('.', '_').replace(',', '_');
		id = id.replaceAll("\\[\\]", "Array");
		id = id.replaceAll("_", "");

		return id;
	}

	/**
	 * Get a list of generic types (type parameters) represented by the give Type object. The Type parameter
	 * is scanned for various sub-types (such as {@link ParameterizedType}, {@link WildcardType},
	 * {@link TypeVariable}, {@link GenericArrayType} and {@link Class} types) to construct the list of the
	 * type parameters of Type object. For example, if Type represents java.util.Map&lt;java.lang.String,
	 * java.util.Date&gt;, then the returned list will contain the type parameters java.lang.String (at index
	 * 0) and java.util.Date (at index 1). Note that the returned List will only contain the actual Type for
	 * each type. In other words, the returned List itself will not contain wildcard or variable types.
	 *
	 * <br/>
	 * <br/>
	 * An empty List of type objects is returned if no generic types are found for the given Type.
	 *
	 * @param type
	 *            The given Type object for which the generic types are sought
	 * @return List of Type objects, each representing the actual Type representing the type parameters of
	 *         given Type object, in that order.
	 * @throws RuntimeException
	 *             if the type parameter is of a type that is not recognized.
	 */
	public static List<Type> getGenericTypes(Type type)
	{
		List<Type> genericTypes = new ArrayList<Type>();

		if(type instanceof ParameterizedType)
		{
			ParameterizedType ptype = (ParameterizedType) type;

			// ignore type type arguments of java.lang.Enum, which seems to be the only
			// case that goes into an infinite recursive loop
			// TODO figure out a solution such that Enum does not have to be ignored
			//
			if(! ptype.getRawType().equals(Enum.class))
			{
				for(Type paramType : ptype.getActualTypeArguments())
					genericTypes.add(paramType);
			}
		}
		else if(type instanceof WildcardType)
		{
			Type[] bounds = ((WildcardType) type).getUpperBounds();
			if(bounds != null && bounds.length > 0)
				genericTypes.addAll(getGenericTypes(bounds[0]));
			else
				genericTypes.add(Object.class);
		}
		else if(type instanceof TypeVariable)
		{
			Type[] bounds = ((TypeVariable<?>) type).getBounds();
			if(bounds != null && bounds.length > 0)
				genericTypes.addAll(getGenericTypes(bounds[0]));
			else
				genericTypes.add(Object.class);
		}
		else if(type instanceof GenericArrayType)
		{
			genericTypes.addAll(getGenericTypes(((GenericArrayType) type).getGenericComponentType()));
		}
		else if(type instanceof Class)
		{
			; // ignore Class type Type objects - see getDerivedGenericTypes();
		}
		else
		{
			throw new RuntimeException("Unknown implementation of java.lang.Type : " + type.toString());
		}

		return genericTypes;
	}

	public static List<Type> getDerivedGenericTypes(Type type)
	{
		List<Type> genericTypes = new ArrayList<Type>();

		if(type instanceof Class)
		{
			Class<?> clazz = (Class<?>) type;
			TypeVariable<?>[] tvars = clazz.getTypeParameters();

			if(tvars != null && tvars.length > 0)
			{
				for(TypeVariable<?> tvar : tvars)
				{
					Type[] bounds = tvar.getBounds();

					if(bounds != null && bounds.length > 0)
						genericTypes.add(bounds[0]);
					else
						genericTypes.add(Object.class);
				}
			}
		}

		return genericTypes;
	}

	/**
	 * Get the underlying Class represented by the given Type. The Type parameter is scanned for various
	 * sub-types (such as {@link ParameterizedType}, {@link WildcardType}, {@link TypeVariable},
	 * {@link GenericArrayType} and {@link Class} types) and the underlying Class object is found. <br/>
	 * <br/>
	 * If Type represents ParameterizedType, the underlying type of the raw type is returned. For example, if
	 * Type represents java.util.List&lt;java.lang.String&gt;, the returned class would be the underlying type
	 * of the raw type (java.util.List), i.e., java.lang.List. <br/>
	 * <br/>
	 * If Type represents a variable type (such as {@link WildcardType} or {@link TypeVariable}) the returned
	 * Class is the upper bound of the given variable or Object.class if no upper bound can be found. For
	 * example, "? extends Number" will return java.lang.Number, but '?' will return Object.class. Similarly,
	 * '&lt;T&gt; T' will return Object.class. <br/>
	 * <br/>
	 * If Type represents a GenericArrayType object, the returned class is the underlying class of the generic
	 * component type. For example, if Type represents '&lt;T&gt; T[]', the returned class would be the
	 * underlying class of the generic component type T.
	 *
	 * @param type
	 *            Type object for which the underlying class is sought
	 * @return Underlying class represented by the given Type
	 * @throws RuntimeException
	 *             if the type parameter is of a type that is not recognized.
	 */
	public static Class<?> getUnderlyingClass(Type type)
	{
		Class<?> underlyingClass = null;
		if(type instanceof ParameterizedType)
		{
			underlyingClass = getUnderlyingClass(((ParameterizedType) type).getRawType());
		}
		else if(type instanceof WildcardType)
		{
			Type[] bounds = ((WildcardType) type).getUpperBounds();
			if(bounds != null && bounds.length > 0)
				underlyingClass = getUnderlyingClass(bounds[0]);
			else
				underlyingClass = Object.class;
		}
		else if(type instanceof TypeVariable)
		{
			Type[] bounds = ((TypeVariable<?>) type).getBounds();
			if(bounds != null && bounds.length > 0)
				underlyingClass = getUnderlyingClass(bounds[0]);
			else
				underlyingClass = Object.class;
		}
		else if(type instanceof GenericArrayType)
		{
			underlyingClass = getUnderlyingClass(((GenericArrayType) type).getGenericComponentType());
		}
		else if(type instanceof Class)
		{
			underlyingClass = (Class<?>) type;
		}
		else
		{
			throw new RuntimeException("Unknown implementation of java.lang.Type : " + type.toString());
		}

		return underlyingClass;
	}

	/**
	 * Get the dimensions of the given type object. If type presents an array, the return value would
	 * be equal to the dimensions of the array. If the type represents a non-array type, the return value
	 * would be 0.
	 *
	 * @param type
	 *            Type object for which the array dimensions are sought
	 * @return Array dimensions of the given type object, or 0 if given type object does not represent an
	 *         array.
	 */
	public static int getArrayDimensions(Type type)
	{
		if(type instanceof GenericArrayType)
		{
			String toString = type.toString();
			int lastOpenBr = toString.indexOf('[');
			int lastClosingAngle = toString.lastIndexOf('>');
			if(lastOpenBr > -1 && lastOpenBr > lastClosingAngle)
			{
				int gap = toString.length() - lastOpenBr;
				return (gap % 2 != 0 ? 0 : (gap/2));
			}
			else
			{
				return 0;
			}
		}
		else
		{
			return getArrayDimensions(getUnderlyingClass(type));
		}
	}

	/**
	 * Get the dimensions of the given Class object. If arrayClass presents an array, the return value
	 * would be equal to the dimensions of the array. If the type represents a non-array type, the return
	 * value would be 0.
	 *
	 * @param arrayClass
	 *            Class object for which the array dimensions are sought
	 * @return Array dimensions of the given arrayClass object, or 0 if given arrayClass object does not
	 *         represent an array
	 */
	public static int getArrayDimensions(Class<?> arrayClass)
	{
		StringBuilder counter = new StringBuilder();
		getArrayDimensions0(arrayClass, counter);
		return counter.toString().length();
	}

	private static void getArrayDimensions0(Class<?> arrayClass, StringBuilder counter)
	{
		if(counter == null) counter = new StringBuilder();
		if(arrayClass.isArray())
		{
			counter.append(' '); // add 1 space
			getArrayDimensions0(arrayClass.getComponentType(), counter);
		}
	}

	/**
	 * Get the root component class of the given array class. If the give class does not represent an array,
	 * the return value will be same as the given value. For example, java.lang.String[] will return
	 * java.lang.String, and java.lang.String will also return java.lang.String. Multi-dimension array class
	 * will return the same root Class. For example, java.lang.String[][][] will return java.lang.String.
	 *
	 * @param arrayClass
	 *            Class object for which the root component class is sought
	 * @return Root component class of the array
	 */
	public static Class<?> getRootComponentClass(Class<?> arrayClass)
	{
		if(arrayClass.isArray())
		{
			return getRootComponentClass(arrayClass.getComponentType());
		}
		else
		{
			return arrayClass;
		}
	}

	/**
	 * Creates a human readable (and compiler readable) class name from the given Class parameter. This method
	 * applies mainly to array types.
	 *
	 * <pre>
	 * <code>
	 *    String[] strArr = { "a", "b", "c" };
	 *    System.out.println(strArr.getClass().getName());
	 * </code>
	 * </pre>
	 *
	 * Would output: <code>[Ljava.lang.String;</code><br/>
	 * <br/>
	 * This is neither human readable nor compiler readable. Instead,
	 *
	 * <pre>
	 * <code>
	 *    String[] strArr = { "a", "b", "c" };
	 *    System.out.println(getDisplayClassName(strArr.getClass()));
	 * </code>
	 * </pre>
	 *
	 * Would output: <code>java.lang.String[].</code><br/>
	 * <br/>
	 * This method supports multi-dimension arrays as well.<br/>
	 * <br/>
	 * If clazz parameter represents a non-array Class, then the output will be same as the output of
	 * <code>getClassName(Class&lt;?&gt;)</code> method.
	 *
	 * @param clazz
	 *            Class representing the Java Class
	 * @return Human and compiler readable name of the clazz (especially, array types)
	 */
	public static String getDisplayClassName(Class<?> clazz)
	{
		if(! clazz.isArray()) return getClassName(clazz);
		else
		{
			Class<?> rootClass = getRootComponentClass(clazz);
			int dim = getArrayDimensions(clazz);
			return getClassName(rootClass) + Utils.chain("[]", dim);
		}
	}

	/**
	 * Returns a comma separate list of Strings, where each value represents the display name of the Class
	 * parameter from the input array, in that order. See comments for getDisplayClassName() method for
	 * details.
	 *
	 * @param classes
	 *            Array of Java Class objects
	 * @return Comma separated list of display names of each input class
	 */
	public static String getDisplayClassNameCSV(Class<?>[] classes)
	{
		StringBuilder sb = new StringBuilder();
		for(int i = 0; classes != null && i < classes.length; i++)
		{
			if(i > 0) sb.append(",");
			sb.append(getDisplayClassName(classes[i]));
		}
		return sb.toString();
	}

	/**
	 * Returns an array of Strings, where each value represents the display name of the Class parameter from
	 * the input array, in that order. See comments for getDisplayClassName() method for details.
	 *
	 * @param classes
	 *            Array of Java Class objects
	 * @return array of display names of each input class
	 */
	public static String[] getDisplayClassNames(Class<?>[] classes)
	{
		String[] classNames = classes != null ? new String[classes.length] : null;
		for(int i = 0; classes != null && i < classes.length; i++)
		{
			classNames[i] = getDisplayClassName(classes[i]);
		}
		return classNames;
	}

	/**
	 * Short-hand method for fetching the package name of the given class name.
	 *
	 * @param className
	 *            Fully qualified name of the Java Class
	 * @return Package name of the Java Class represented by the className parameter, or empty string if
	 *         className is a package-less class
	 */
	public static String getPackageName(String className)
	{
		int lastDot = className.lastIndexOf('.');

		return lastDot > -1 ? className.substring(0, lastDot) : "";
	}

	/**
	 * Short-hand method for fetching the package name of the given Class
	 *
	 * @param clazz
	 *            Java Class object
	 * @return Package name of clazz parameter, or empty string if clazz is a package-less class.
	 */
	public static String getPackageName(Class<?> clazz)
	{
		return getPackageName(getClassName(clazz));
	}

	/**
	 * Short-hand method for getting a method of the given clazz without throwing the
	 * NoSuchMethodException.  If the method is not found, a null is returned.
	 *
	 * @param clazz This Class will be searched for the desired method
	 * @param name Name of the method that will be searched
	 * @param paramTypes List of parameter types (Classes)
	 *
	 * @return Method object if the search matches a method, otherwise a null is returned
	 * @see Class.getMethod()
	 */
	public static Method getMethod(Class<?> clazz, String name, Class<?>... paramTypes)
	{
		try
		{
			return clazz.getMethod(name, paramTypes);
		}
		catch(NoSuchMethodException nsme)
		{
			return null;
		}
	}

	/**
	 * Determines if the method with the given methodName on the clazz parameter has overloaded methods. In
	 * other words, if clazz contains more than one methods matching methodName, this method returns true.
	 *
	 * @param clazz
	 *            Java Class for which the check needs to be made
	 * @param methodName
	 *            Name of the method that needs to be matched for overloaded methods
	 * @return true if clazz has more than 1 methods where name is methodName. false, otherwise.
	 */
	public static boolean hasOverloadedMethod(Class<?> clazz, String methodName)
	{
		Method[] methods = clazz.getMethods();

		boolean match = false;
		for(Method method : methods)
		{
			// do not consider bridge or abstract methods as overloaded methods
			//
			if(method.isBridge()) continue;
			if(Modifier.isAbstract(method.getModifiers())) continue;

			if(methodName.equals(method.getName()))
			{
				// if match == true, second match found.
				// return true
				if(match) return true;

				// first match found, continue looking
				match = true;
			}
		}

		// if it did not return before, the method is not overloaded
		return false;
	}

	public static String getStandardTypeName(String fieldType)
	{
		String xsdType = fieldType;

		if(fieldType.equals("boolean") || fieldType.equals("java.lang.Boolean"))
			xsdType = "boolean";
		else if(fieldType.equals("byte") || fieldType.equals("java.lang.Byte"))
			xsdType = "byte";
		else if(fieldType.equals("char") || fieldType.equals("java.lang.Character"))
			xsdType = "char";
		else if(fieldType.equals("short") || fieldType.equals("java.lang.Short"))
			xsdType = "short";
		else if(fieldType.equals("int") || fieldType.equals("java.lang.Integer"))
			xsdType = "int";
		else if(fieldType.equals("float") || fieldType.equals("java.lang.Float"))
			xsdType = "float";
		else if(fieldType.equals("long") || fieldType.equals("java.lang.Long"))
			xsdType = "long";
		else if(fieldType.equals("double") || fieldType.equals("java.lang.Double"))
			xsdType = "double";
		else if(fieldType.equals("java.lang.String"))
			xsdType = "string";
		else if(fieldType.equals("java.lang.StringBuilder"))
			xsdType = "string";
		else if(fieldType.equals("java.lang.StringBuffer"))
			xsdType = "string";

		return xsdType;
	}

	public static void main(String[] args)
	{
		System.out.println(getPackageName(String.class));

		System.out.println(hasOverloadedMethod(Element.class, "addContent"));

		String[] strArr = { "x" };
		System.out.println(getUnderlyingClass(strArr.getClass()));
	}
}
